// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Net.Http.HPack;
using System.Net.Http.QPack;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack;

internal sealed class EncoderStreamReader
{
    private enum State
    {
        Ready,
        DynamicTableCapcity,
        NameIndex,
        NameLength,
        Name,
        ValueLength,
        ValueLengthContinue,
        Value,
        Duplicate
    }

    //0   1   2   3   4   5   6   7
    //+---+---+---+---+---+---+---+---+
    //| 0 | 0 | 1 |   Capacity(5+)   |
    //+---+---+---+-------------------+
    private const byte DynamicTableCapacityMask = 0xE0;
    private const byte DynamicTableCapacityRepresentation = 0x20;
    private const byte DynamicTableCapacityPrefixMask = 0x1F;
    private const int DynamicTableCapacityPrefix = 5;

    //0   1   2   3   4   5   6   7
    //+---+---+---+---+---+---+---+---+
    //| 1 | S |    Name Index(6+)    |
    //+---+---+-----------------------+
    //| H |     Value Length(7+)     |
    //+---+---------------------------+
    //|  Value String(Length bytes)  |
    //+-------------------------------+
    private const byte InsertWithNameReferenceMask = 0x80;
    private const byte InsertWithNameReferenceRepresentation = 0x80;
    private const byte InsertWithNameReferencePrefixMask = 0x3F;
    private const byte InsertWithNameReferenceStaticMask = 0x40;
    private const int InsertWithNameReferencePrefix = 6;

    //0   1   2   3   4   5   6   7
    //+---+---+---+---+---+---+---+---+
    //| 0 | 1 | H | Name Length(5+)  |
    //+---+---+---+-------------------+
    //|  Name String(Length bytes)   |
    //+---+---------------------------+
    //| H |     Value Length(7+)     |
    //+---+---------------------------+
    //|  Value String(Length bytes)  |
    //+-------------------------------+
    private const byte InsertWithoutNameReferenceMask = 0xC0;
    private const byte InsertWithoutNameReferenceRepresentation = 0x40;
    private const byte InsertWithoutNameReferencePrefixMask = 0x1F;
    private const byte InsertWithoutNameReferenceHuffmanMask = 0x20;
    private const int InsertWithoutNameReferencePrefix = 5;

    //0   1   2   3   4   5   6   7
    //+---+---+---+---+---+---+---+---+
    //| 0 | 0 | 0 |    Index(5+)     |
    //+---+---+---+-------------------+
    private const byte DuplicateMask = 0xE0;
    private const byte DuplicateRepresentation = 0x00;
    private const byte DuplicatePrefixMask = 0x1F;
    private const int DuplicatePrefix = 5;

    private const int StringLengthPrefix = 7;
    private const byte HuffmanMask = 0x80;

    private bool _s;
    private readonly byte[] _stringOctets;
    private readonly byte[] _headerNameOctets;
    private readonly byte[] _headerValueOctets;
    private byte[]? _headerName;
    private int _headerNameLength;
    private int _headerValueLength;
    private int _stringLength;
    private int _stringIndex;
    private readonly DynamicTable _dynamicTable = new DynamicTable(1000); // TODO figure out architecture.

    private IntegerDecoder _integerDecoder;
    private State _state = State.Ready;
    private bool _huffman;

    public EncoderStreamReader(int maxRequestHeaderFieldSize)
    {
        // TODO how to propagate dynamic table around.

        _stringOctets = new byte[maxRequestHeaderFieldSize];
        _headerNameOctets = new byte[maxRequestHeaderFieldSize];
        _headerValueOctets = new byte[maxRequestHeaderFieldSize];
    }

    public void Read(ReadOnlySequence<byte> data)
    {
        foreach (var segment in data)
        {
            var span = segment.Span;
            for (var i = 0; i < span.Length; i++)
            {
                OnByte(span[i]);
            }
        }
    }

    private void OnByte(byte b)
    {
        int intResult;
        int prefixInt;
        switch (_state)
        {
            case State.Ready:
                if ((b & DynamicTableCapacityMask) == DynamicTableCapacityRepresentation)
                {
                    prefixInt = DynamicTableCapacityPrefixMask & b;
                    if (_integerDecoder.BeginTryDecode((byte)prefixInt, DynamicTableCapacityPrefix, out intResult))
                    {
                        OnDynamicTableCapacity(intResult);
                    }
                    else
                    {
                        _state = State.DynamicTableCapcity;
                    }
                }
                else if ((b & InsertWithNameReferenceMask) == InsertWithNameReferenceRepresentation)
                {
                    prefixInt = InsertWithNameReferencePrefixMask & b;
                    _s = (InsertWithNameReferenceStaticMask & b) == InsertWithNameReferenceStaticMask;

                    if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithNameReferencePrefix, out intResult))
                    {
                        OnNameIndex(intResult);
                    }
                    else
                    {
                        _state = State.NameIndex;
                    }
                }
                else if ((b & InsertWithoutNameReferenceMask) == InsertWithoutNameReferenceRepresentation)
                {
                    prefixInt = InsertWithoutNameReferencePrefixMask & b;
                    _huffman = (InsertWithoutNameReferenceHuffmanMask & b) == InsertWithoutNameReferenceHuffmanMask;

                    if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithoutNameReferencePrefix, out intResult))
                    {
                        OnStringLength(intResult, State.Name);
                    }
                    else
                    {
                        _state = State.NameIndex;
                    }
                }
                else if ((b & DuplicateMask) == DuplicateRepresentation)
                {
                    prefixInt = DuplicatePrefixMask & b;
                    if (_integerDecoder.BeginTryDecode((byte)prefixInt, DuplicatePrefix, out intResult))
                    {
                        OnDuplicate(intResult);
                    }
                    else
                    {
                        _state = State.Duplicate;
                    }
                }
                break;
            case State.DynamicTableCapcity:
                if (_integerDecoder.TryDecode(b, out intResult))
                {
                    OnDynamicTableCapacity(intResult);
                }
                break;
            case State.NameIndex:
                if (_integerDecoder.TryDecode(b, out intResult))
                {
                    OnNameIndex(intResult);
                }
                break;
            case State.NameLength:
                if (_integerDecoder.TryDecode(b, out intResult))
                {
                    OnStringLength(intResult, nextState: State.Name);
                }
                break;
            case State.Name:
                _stringOctets[_stringIndex++] = b;

                if (_stringIndex == _stringLength)
                {
                    OnString(nextState: State.ValueLength);
                }

                break;
            case State.ValueLength:
                _huffman = (b & HuffmanMask) != 0;

                // TODO confirm this.
                if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult))
                {
                    OnStringLength(intResult, nextState: State.Value);
                    if (intResult == 0)
                    {
                        ProcessValue();
                    }
                }
                else
                {
                    _state = State.ValueLengthContinue;
                }
                break;
            case State.ValueLengthContinue:
                if (_integerDecoder.TryDecode(b, out intResult))
                {
                    OnStringLength(intResult, nextState: State.Value);
                    if (intResult == 0)
                    {
                        ProcessValue();
                    }
                }
                break;
            case State.Value:
                _stringOctets[_stringIndex++] = b;
                if (_stringIndex == _stringLength)
                {
                    ProcessValue();
                }
                break;
            case State.Duplicate:
                if (_integerDecoder.TryDecode(b, out intResult))
                {
                    OnDuplicate(intResult);
                }
                break;
        }
    }

    private void OnStringLength(int length, State nextState)
    {
        if (length > _stringOctets.Length)
        {
            throw new QPackDecodingException(/*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/);
        }

        _stringLength = length;
        _stringIndex = 0;
        _state = nextState;
    }

    private void ProcessValue()
    {
        OnString(nextState: State.Ready);
        var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
        var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
        DynamicTable.Insert(headerNameSpan, headerValueSpan);
    }

    private void OnString(State nextState)
    {
        int Decode(byte[] dst)
        {
            if (_huffman)
            {
                return Huffman.Decode(new ReadOnlySpan<byte>(_stringOctets, 0, _stringLength), ref dst);
            }
            else
            {
                Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
                return _stringLength;
            }
        }

        try
        {
            if (_state == State.Name)
            {
                _headerName = _headerNameOctets;
                _headerNameLength = Decode(_headerNameOctets);
            }
            else
            {
                _headerValueLength = Decode(_headerValueOctets);
            }
        }
        catch (HuffmanDecodingException ex)
        {
            throw new QPackDecodingException(""/*CoreStrings.QPackHuffmanError*/, ex);
        }

        _state = nextState;
    }

    private void OnNameIndex(int index)
    {
        var header = GetHeader(index);
        _headerName = header.Name;
        _headerNameLength = header.Name.Length;
        _state = State.ValueLength;
    }

    private void OnDynamicTableCapacity(int dynamicTableSize)
    {
        // Call Decoder to update the table size.
        DynamicTable.Resize(dynamicTableSize);
        _state = State.Ready;
    }

    private void OnDuplicate(int index)
    {
        _dynamicTable.Duplicate(index);
        _state = State.Ready;
    }

    private System.Net.Http.QPack.HeaderField GetHeader(int index)
    {
        try
        {
            return _s ? H3StaticTable.Get(index) : _dynamicTable[index];
        }
        catch (IndexOutOfRangeException ex)
        {
            throw new QPackDecodingException("" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index)*/, ex);
        }
    }
}
